Add hosted captun.sh safety controls#20
Conversation
…s' into mmkal/26/05/23/hosted-rate-limits
commit: |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4377903. Configure here.
| () => | ||
| settle(() => { | ||
| void webSocketConnectionFailedError(options).then(reject); | ||
| }), |
There was a problem hiding this comment.
Close handler skips rejection probe
Medium Severity
In waitUntilOpen, the close listener rejects immediately with a generic close message, while the diagnostic HTTP probe that surfaces 409/rate-limit bodies runs only from the error listener. If close fires first (common when the upgrade handshake fails), CaptunTunnelConnectError and CLI active-name conflict hints are skipped.
Reviewed by Cursor Bugbot for commit 4377903. Configure here.
| windowSeconds: config.windowSeconds, | ||
| }); | ||
| if (!result.ok) return hostedRateLimitedResponse(result); | ||
| } |
There was a problem hiding this comment.
Rate limit counts before rejection
Medium Severity
Hosted forwarded-request limiting runs separate HostedRateLimiter.check calls for the client IP and tunnel name in sequence. Each successful check increments its bucket before the next runs, so a request rejected on the tunnel limit still consumes IP quota, and vice versa, without the request being forwarded.
Reviewed by Cursor Bugbot for commit 4377903. Configure here.
|
Superseded by the gateway-owned addressing refactor now pushed to #16. The hosted safety/rate-limit work should be rebuilt on top of the new gateway/token/connect-query protocol instead of carrying forward ownerToken/serverUrl-era API shape. |
## Summary Adds the hosted `captun.sh` path so a first-time user can get a public tunnel without deploying anything first. ```bash # with something listening on localhost:3000 npx captun 3000 ``` The CLI now defaults to the hosted `https://captun.sh` gateway when there is no local config, generates a random tunnel name, and prints a public URL like: ```text https://abc123.captun.sh -> localhost:3000 ``` No Cloudflare account, config file, token, or deploy wizard is needed for that first run. Users can still run `npx captun deploy` later if they want their own self-hosted gateway. ## What Changed - defaults missing local config to the hosted `https://captun.sh` gateway - changes `createCaptunTunnel` to connect with `{ gateway, name, token, fetch }` and wait for the gateway to return `{ url, token }` - renames the low-level Cap'n Web accept APIs to `acceptFetcherCapability` and `acceptFetcherCapabilityFromSocket` - updates the CLI, deploy wizard, config file, smoke scripts, benchmarks, docs, and hosted browser demo to use `gateway`/`token` - keeps the self-hosted Cloudflare Tunnel Gateway readable in `src/worker.ts`, with the hosted `captun.sh` product surface isolated under `src/hosted/` - reserves product/control-plane tunnel names, serves `www.captun.sh`, and redirects the apex host to `www` - adds `CONTEXT.md` and ADR-0001 to keep the Fetcher Capability / Tunnel / Gateway language clear #20 is superseded by this shape; hosted rate limiting and ownership controls are rebuilt in #22 on top of the gateway-owned protocol. ## Library Example ```ts import { createCaptunTunnel } from "captun"; const tunnel = await createCaptunTunnel({ fetch: (request) => Response.json({ path: new URL(request.url).pathname }), }); console.log(tunnel.url); // https://abc123.captun.sh ``` Self-hosted use now passes the gateway URL, not a tunnel URL template: ```bash npx captun 3000 --gateway 'https://captun.youraccount.workers.dev' --token abc123 ``` ## Structure - `src/index.ts` contains the client API and low-level Fetcher Capability helpers. - `src/worker.ts` is the deployable self-hosted Cloudflare Tunnel Gateway. - `src/hosted/site.ts` and `src/hosted/worker.ts` are the Iterate-operated hosted surface for `captun.sh`. - `wrangler.jsonc` is for self-hosted deployment; `wrangler.hosted.jsonc` is for the hosted service. ## Verification - `pnpm run check` - `pnpm test` - `pnpm run build` - `pnpm exec vitest run test/worker.test.ts test/e2e.test.ts examples/weather-reporter/e2e.test.ts` - `CAPTUN_PUBLIC_E2E=1 pnpm exec vitest run test/public-hosted.test.ts` - deployed `captun-public` to Iterate prd with `captun.sh/*` and `*.captun.sh/*`


Summary
Adds the first hosted
captun.shsafety layer on top of the initial public tunnel deployment.This collapses the earlier stacked safety PRs into one review surface. It supersedes #17, #18, and #19.
Behavior
Hosted
captun.shnow gets:HOSTED_RATE_LIMIT_DISABLED=1is explicitly setownerToken409 Conflictinstead of anonymous clients evicting each other from an active tunnel nameSelf-hosted and secret-protected deployments keep the existing replacement/auth behavior.
User-Facing API
createCaptunTunnelnow returns the anonymous hosted owner token and accepts it for intentional same-owner replacement:A different active anonymous owner gets a conflict instead of evicting the current tunnel.
Runtime Configuration
Defaults:
HOSTED_RATE_LIMIT_WINDOW_SECONDS=60HOSTED_CONNECTS_PER_IP_PER_WINDOW=30HOSTED_REQUESTS_PER_IP_PER_WINDOW=600HOSTED_REQUESTS_PER_TUNNEL_PER_WINDOW=1200The hosted rate limiter is wired through a new
HostedRateLimiterDurable Object binding and migration.Follow-Up Left Out Intentionally
This is still intentionally lightweight. It does not add persistent reservations, auth/accounts, paid custom names, active tunnel caps, byte/response caps, observability dashboards, or Cloudflare-native edge throttles. Anonymous ownership is active-session only: once a tunnel disconnects, the name is free for another anonymous token to claim.
Verification
pnpm exec vitest run test/hosted-admission.test.ts test/worker.test.ts test/cli.test.ts --testNamePattern 'diagnostic|retries reuse|secret|rate limit|ownership|owner token'pnpm run checkpnpm testpnpm run buildCAPTUN_PUBLIC_E2E=1 pnpm vitest run test/public-hosted.test.tswas run on the earlier safety stack before the Bugbot-only fixesNote
High Risk
Adds enforcement and throttling to the hosted tunnel connect/request path (new Durable Object rate limiter, ownership-token admission, and diagnostic probing), which can affect availability and connection behavior for production traffic. Also changes the public client API surface (
ownerToken), so regressions could break existing consumers if assumptions differ.Overview
Introduces hosted-only safety controls for
captun.sh: anonymous tunnel connects now require an ownership token and reject conflicting active owners with409instead of evicting the current session, while self-hosted/secret-protected deployments keep prior replace/auth behavior.Adds Durable Object–backed fixed-window rate limiting for hosted connect attempts (per IP) and forwarded requests (per IP and per tunnel), including fail-closed behavior when the limiter binding is missing (with an explicit
HOSTED_RATE_LIMIT_DISABLED=1escape hatch).Improves client-side ergonomics and diagnostics:
createCaptunTunnelnow generates/returns/acceptsownerToken, performs a short read-only HTTP probe to surface WebSocket upgrade rejection details (CaptunTunnelConnectError), and the CLI reuses one token across retries and shows a clearer message for the known hosted name-in-use conflict.Reviewed by Cursor Bugbot for commit 4377903. Bugbot is set up for automated code reviews on this repo. Configure here.